import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { apiSafe, type Meta, type PartyForecast } from "@/lib/api"; import { SeatDistribution } from "@/components/charts"; import { PartyLogo } from "@/components/party"; import { Badge, Card, Dual, Empty, KV, SectionHeader } from "@/components/ui"; import { dateFr, int, pct, prob } from "@/lib/format"; import { PARTIES, partyLabel } from "@/lib/parties"; export const revalidate = 120; export function generateStaticParams() { return PARTIES.map((p) => ({ id: p.id })); } interface Detail { id: string; abbreviation: string; officialName: string; color: string; website: string; leader: string | null; leaderSince: string | null; dgeqName: string | null; candidatesCount: number; seats2022: number; vote2022: number | null; colorNote: string; forecast: PartyForecast | null; strongholds: { code: number; name: string; slug: string; p: number; region: string }[]; targets: { code: number; name: string; slug: string; margin: number; favorite: string }[]; regions: Record; candidates: { riding: number; name: string; incumbent: boolean }[]; documents: { id: number; title: string; url: string; type: string; date: string | null; proposals: number }[]; topics: Record; axisProfile: Record; history: Record } export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; const p = PARTIES.find((x) => x.id === id); return { title: p ? `${p.officialName} — projection, candidatures, documents` : "Parti" }; } export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const [d, meta] = await Promise.all([apiSafe(`/api/parties/${id}`), apiSafe("/api/meta")]); if (!d) notFound(); if (!meta) return ; const f = d.forecast; const topics = Object.entries(d.topics).sort((a, b) => b[1] - a[1]).slice(0, 8); return (
← Partis

{d.officialName}

Chef·fe (registre DGEQ) : {d.leader ?? "—"}{d.leaderSince ? ` · depuis le ${dateFr(d.leaderSince)}` : ""}{d.dgeqName && d.dgeqName !== d.officialName ? · nom au registre : {d.dgeqName} : null}
{d.website.replace(/^https?:\/\//, "")}
{f && (
Sièges
{f.seatsMedian}
80 % {f.seats80[0]}–{f.seats80[1]}
Vote
{f.voteMean.toFixed(1).replace(".", ",")}
± {f.voteSd.toFixed(1).replace(".", ",")} · 2022 : {pct(d.vote2022)}
Majorité
{prob(f.pMajority)}
seuil {meta.majority}
Plus de sièges
{prob(f.pMostSeats)}
2022 : {d.seats2022} sièges
)}
{f?.seatDistribution &&
} wide={} />
}
{d.strongholds.slice(0, 40).map((r) => )}{d.strongholds.length === 0 && }
{r.name}{prob(r.p)}
Aucune circonscription où le parti est favori.
{d.strongholds.length} circonscriptions
{d.targets.map((r) => )}
{r.name}derrière {partyLabel(r.favorite)}{r.margin.toFixed(1).replace(".", ",")} pt
{Object.entries(d.regions).sort((a, b) => b[1] - a[1]).map(([g, v]) => (
{g.replace(/-/g, " ")}
{v.toFixed(0)} %
))}
Tous les documents →} /> {d.documents.length === 0 ?
Aucun document analysé pour l'instant : le connecteur documentaire vérifie le site officiel toutes les deux heures.
: {d.documents.slice(0, 12).map((x) => )}
DocumentTypePropositions
{x.title}{x.type}{x.proposals}
}
{topics.length > 0 &&
{topics.map(([t, n]) => {meta.topics[t] ?? t} {n})}
}
{Object.keys(d.axisProfile).length === 0 ?
Positions en cours d'extraction.
:
{Object.entries(meta.axes).map(([axis, a]) => { const v = d.axisProfile[axis]; return (
{a.left}{a.label}{a.right}
{v &&
}
{!v &&
Aucun énoncé documenté.
}
); })}
}
{d.candidates.map((c) => )}
{c.name}{c.incumbent && sortant·e}fiche
Fiche
`${y.slice(0, 4)} : ${n}`).join(" · ") || "—"], ["Couleur QC26", {d.color} {d.colorNote}]]} />
); }